12. 面向对象编程 Object-Oriented Programming
面向对象编程是一种模块化的组织程序的方法,其借助抽象屏障,通过将信息与方法封装到一个对象中以实现不同部分代码的相互协作。面向对象编程具有分布式计算的思想。即,每个对象有自己的局部状态,并通过其方法更新这些状态。对方法的调用是一种在对象之间传递信息的方法。多个对象可能是某个公共类型的实例。类型不同的对象之间相互可以存在联系。
类是对象实例的模板,每个对象都是某个类的实例。
在 Python 中,声明一个类的格式如下:
class <类名>:
<类体>
声明一个类时,类内部的语句会 立刻执行,而生成的类对象将会被绑定在当前帧的 <类名> 上。类内部的赋值与定义语句为该类建立了相关的 属性。
与 Java 略微不同的是,Python 中的构造函数的命名为 __init__,该方法要求将该类本身 self (等同于 Java 中的 this)作为其第一个参数,其余参数按顺序排列在后面。
将定义的类名作为调用表达式的操作数,即可新建、调用构造函数 __init__ 后返回一个该类的实例。在此过程中:
- 新建了一个 空类,并作为参数传递给构造函数,绑定在第一个参数
self上。 - 执行构造函数,以为该空类分配一些必要的属性。
- 构造函数执行完成后,返回
self作为新建的对象实例。
每一次对类的调用都将新建并返回一个新的该类实例。所有新建的实例相互之间相互独立。
Python 有一个内置函数 getattr(obj, attrname),其作用等同于点表达式。另有内置函数 hasattr(obj, arrtname),检查某个对象是否具有一个特定的属性。
接下来需要对函数与绑定到对象的方法加以区分:即 对象 + 函数 = 绑定到对象的方法。
具体而言,当声明一个类的某个函数时,其第一个参数需要总是设为 self,代表该类的某个对象实例。这是因为当我们通过类名去调用某个类的函数时,Python 解释器将其当作一个普通的函数对待,需要我们传入想要该函数操作的对象与其它参数。
但当通过点表达式调用对象的方法时,Python 解释器将其作为 绑定到一个对象的方法 对待,并在调用时将自动把该对象作为该方法的第一个参数传入,后面跟着其余的参数。这代表 Python 解释器已经自动将类中的函数作为方法绑定到了某个对象上。
所以,当我们调用点表达式 <expression>.<name> 时,实际发生了如下的过程:
- 对
<expression>求值,得到一个对象。 - 在该对象的属性中查找是否存在字段
<name>,如果存在,返回对应的值。 - 如果不存在,在该对象所属的类中是否存在字段
<name>,如果存在,返回对应的值。若字段<name>指向了一个函数,则返回与类对象绑定的方法。
显然,类中可以不只定义函数,也可以定义一些值。这被称为 类属性(Class Attributes),其是类的一部分而非类的某个实例的一部分,因此在该类的所有实例中共享。
然而需要注意的是,对对象实例与类的赋值操作与求值操作不同:当我们通过 <expression>.<name> = <value> 去给一个对象实例或类赋值时,实际发生的情况是:
- 如果
<name>是一个 对象,则给该对象实例的对应字段赋以相应的值。 - 如果
<name>是一个 类,则给该类的对应字段赋以相应的值。
因此,假如我们进行一个形如 <对象名>.<类中的某个字段> = <value> 的赋值操作的话,实际发生的是在该对象实例内部创建了一个与 类中的该字段同名 的属性。而类中的对应字段 没有受到任何影响。
class interest:
interest = 0.02
tom = account()
john = account()
>>> tom.interest
0.02
>>> tom.interest = 0.05
>>> tom.interest
0.05
>>> john.interest
0.02
>>> account.another_interest = 0.3
>>> john.another_interest
0.3
>>> tom.another_interest
0.3
继承(Inheritance) 是面向对象编程中的一个重要概念,可以将多个类联系在一起。Python 的继承是 多继承 的,一个类可以有有多个父类。继承某个类的写法如下:
class <name>(<parent class A>,...):
<suite>
...
子类可以继承父类的所有属性,并根据需要重写它们,或添加自己专有的属性。当然,子类并不是直接复制了子类的属性。而是在查找类的某个字段时,会先在本类中查找,若没有找到,则递归的在其父类中查找。构造函数__init__同理。当某个类没有构造函数时,其将随继承关系向上查找第一个有继承关系的父类并将该构造方法设置为自己的构造方法。
在Python中,多继承的方法调用顺序由 方法解析顺序(Method Resolution Order, MRO) 决定。这一顺序可以通过类的 __mro__ 属性查看。详见:类与继承。其核心规则是:
- 子类优先于父类:子类的方法或属性会优先被调用。
- 从左到右:继承的父类顺序(从左到右定义)决定了优先级。
- 避免重复和循环:C3线性化算法(C3 Linearization)保证继承顺序的一致性,避免循环依赖。
继承与组合是两种不同的组合多种对象的方式。继承代表了“A 是一种 B”的关系;而组合代表了“A 有 B“ 的关系,即一个对象将另一种对象作为属性。
多态(Polymorphic) 同样是面向对象编程中的一个重要概念。其指对同一个方法的调用由于对象不同,会有不同的行为。str 与 repr 是典型的多态函数。这两个函数能够实现多态,根本在于 Python 给每一个 类 内置了 __repr__ 方法。(更严谨的说法是 Python 中所有类都有一个共同父类 object 而 object 有一个 object.__repr__() 方法)因此将某个对象传入上述两个函数中时,这些函数只是反过来调用的对象所属的类自带的 __str__ 与 __repr__ 方法。Python 解释器不会自动创建 __str__ 方法,如果某个类没有找到 __str__ 方法,则会调用 __repr__ 方法。
了解上述内容后,我们可以自己写出 repr 函数与 str 函数:
def repr(*args):
return [x.__repr__() for x in args]
def str(*args):
out = []
for x in args:
if hasattr(x, __str__):
out.append(x.__str__())
else:
out.append(x.__repr__())
Python 中存在多种内置函数,代表与某些内置系统的交互动作,例如:
__init__:用于生成对象实例的构造方法。__str__:用于str()函数,返回某个对象的“非标准”字符串表示。__repr__:用于repr()函数,返回某个对象的标准化表示。__add__:等同于__radd__(由于加法满足交换律),用于add()函数。__float__:用于float()函数。__bool__:用于bool()函数。
class Tree:
def __init__(self, value, children=[]):
self.value = value
for child in children:
assert Tree.isTree(child), "A tree's children must be trees."
self.children = children
def getNodeValue(self):
return self.value
def getNodeChildren(self):
return self.children
def isTree(self, tree):
if type(tree) != list or len(tree) < 1:
return False
for child in Tree.getNodeChildren(tree):
if not Tree.isTree(child):
return False
return True
def isLeaf(self, tree):
return not Tree.getNodeChildren(tree)